/**
 * Copyright 2018-2019 jianggujin (www.jianggujin.com).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jianggujin.modulelink.util;

import java.io.Closeable;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import com.jianggujin.modulelink.util.JLogFactory.JLog;

/**
 * 模块的ClassLoader，继承自URLClassLoader，不通过父ClassLoader加载，突破双亲委派机制。
 * 
 * @author jianggujin
 *
 */
public class JModuleClassLoader extends URLClassLoader implements Closeable {

    private static final JLog logger = JLogFactory.getLog(JModuleClassLoader.class);
    private final ConcurrentHashMap<String, Object> parallelLockMap;

    private static boolean canCloseJar = false;
    private static boolean registerAsParallelCapable = false;
    private final List<JarURLConnection> cachedJarFiles;

    private JOverideStrategy overideStrategy;

    static {
        // 1.7之后可以直接调用close方法关闭打开的jar，需要判断当前运行的环境是否支持close方法，如果不支持，需要缓存，避免卸载模块后无法删除jar
        try {
            URLClassLoader.class.getMethod("close");
            canCloseJar = true;
            if (logger.isDebugEnabled()) {
                logger.debug("use URLClassLoader#close()");
            }
        } catch (NoSuchMethodException e) {
        } catch (SecurityException e) {
        }
        // 判断是否支持并行加载,1.7之后支持，如果运行在低版本，手动开启
        try {
            ClassLoader.class.getDeclaredMethod("registerAsParallelCapable");
            registerAsParallelCapable = true;
            if (logger.isDebugEnabled()) {
                logger.debug("use ClassLoader#registerAsParallelCapable()");
            }
            ClassLoader.registerAsParallelCapable();
        } catch (NoSuchMethodException e) {
        } catch (SecurityException e) {
        }
    }

    public JModuleClassLoader(Set<URL> urls) {
        super(new URL[0]);
        parallelLockMap = registerAsParallelCapable ? null : new ConcurrentHashMap<String, Object>();
        cachedJarFiles = canCloseJar ? null : new ArrayList<JarURLConnection>();
        if (urls != null) {
            for (URL url : urls) {
                this.addURL(url);
            }
        }
    }

    public JModuleClassLoader(Set<URL> urls, ClassLoader parent) {
        super(new URL[0], parent);
        parallelLockMap = registerAsParallelCapable ? null : new ConcurrentHashMap<String, Object>();
        cachedJarFiles = canCloseJar ? null : new ArrayList<JarURLConnection>();
        if (urls != null) {
            for (URL url : urls) {
                this.addURL(url);
            }
        }
    }

    @Override
    protected void addURL(URL url) {
        if (!canCloseJar) {
            try {
                // 打开并缓存文件url连接
                URLConnection uc = url.openConnection();
                if (uc instanceof JarURLConnection) {
                    uc.setUseCaches(true);
                    ((JarURLConnection) uc).getManifest();
                    cachedJarFiles.add((JarURLConnection) uc);
                }
            } catch (Exception e) {
                logger.error("Failed to cache JAR file:" + url.toExternalForm());
            }
        }
        super.addURL(url);
    }

    /**
     * 覆盖双亲委派机制
     *
     * @see ClassLoader#loadClass(String, boolean)
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> result = null;
        synchronized (getClassLoadingLock(name)) {
            if (getOverideStrategy().isEligibleForOverriding(name)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Load class for overriding: " + name);
                }
                result = loadClassForOverriding(name, resolve);
            }
            if (result != null) {
                // 链接类
                if (resolve) {
                    resolveClass(result);
                }
                return result;
            }
            // 使用默认类加载方式
            return super.loadClass(name, resolve);
        }
    }

    /**
     * 加载一个子模块覆盖的类
     *
     * @param name    类名
     * @param resolve 是否解析
     * @return 类对象
     * @throws ClassNotFoundException 未找到类异常
     */
    protected Class<?> loadClassForOverriding(String name, boolean resolve) throws ClassNotFoundException {
        // 查找已加载的类
        Class<?> result = findLoadedClass(name);
        if (result == null) {
            // 加载类
            try {
                result = findClass(name);
            } catch (ClassNotFoundException e) {
                // 当前子模块系统加载不到时尝试从父容器中加载
                if (logger.isDebugEnabled()) {
                    logger.debug("class will load from parent context: " + name);
                }
                result = super.loadClass(name, resolve);
            }
        }
        return result;
    }

    @Override
    public URL getResource(String name) {
        if (this.getOverideStrategy().isResourceEligibleForOverriding(name)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Load resource for overriding: " + name);
            }
            return this.findResource(name);
        }
        return super.getResource(name);
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        if (this.getOverideStrategy().isResourceEligibleForOverriding(name)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Load resources for overriding: " + name);
            }
            return this.findResources(name);
        }
        return super.getResources(name);
    }

    @Override
    public void close() throws IOException {
        if (canCloseJar) {
            try {
                super.close();
            } catch (IOException ioe) {
                logger.warn("Invoke super close method fail", ioe);
            }
        } else {
            for (JarURLConnection conn : cachedJarFiles) {
                conn.getJarFile().close();
            }
            cachedJarFiles.clear();
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(this.getClass().getSimpleName());
        sb.append("\r\n  excludedPackages: ");
        sb.append(this.getOverideStrategy().getExcludedPackages());
        sb.append("\r\n");
        if (this.getParent() != null) {
            sb.append("----------> Parent Classloader:\r\n");
            sb.append(this.getParent().toString());
        }
        return (sb.toString());
    }

    /**
     * 类加载锁
     * 
     * @param className 类名
     * @return 锁对象
     */
    protected Object getClassLoadingLock(String className) {
        if (registerAsParallelCapable) {
            return super.getClassLoadingLock(className);
        } else {
            Object lock = this;
            if (parallelLockMap != null) {
                Object newLock = new Object();
                lock = parallelLockMap.putIfAbsent(className, newLock);
                if (lock == null) {
                    lock = newLock;
                }
            }
            return lock;
        }
    }

    public JOverideStrategy getOverideStrategy() {
        if (this.overideStrategy == null) {
            this.overideStrategy = new JOverideStrategy();
        }
        return overideStrategy;
    }

    public void setOverideStrategy(JOverideStrategy overideStrategy) {
        this.overideStrategy = overideStrategy;
    }
}
